package com.template.project.config;

import com.template.project.constant.RequestHeaderConst;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 根据SwaggerTagConst内部类动态生成Swagger group
 *
 * @author xs
 */
@Slf4j
@EnableSwagger2
@Configuration
@Conditional(SystemEnvironmentConfig.class)
public class SwaggerConfig implements EnvironmentAware, BeanDefinitionRegistryPostProcessor {
    private String name;
    private String eMail;
    private String title;
    private String description;
    private String version;
    private String teamUrl;
    private String host;
    private String tagClass;

    @Override
    public void setEnvironment(Environment environment) {
        this.name = environment.getProperty("swagger.name");
        this.eMail = environment.getProperty("swagger.e-mail");
        this.title = environment.getProperty("swagger.title");
        this.description = environment.getProperty("swagger.description");
        this.version = environment.getProperty("swagger.version");
        this.host = environment.getProperty("swagger.host");
        this.tagClass = environment.getProperty("swagger.tag-class");
        this.teamUrl = environment.getProperty("swagger.team-url");
    }

    @Override
    public void postProcessBeanDefinitionRegistry(@NotNull BeanDefinitionRegistry registry) {
        buildGroup().forEach((group, apiTags) -> {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(Docket.class,
                    () -> createDocket(group, apiTags));
            registry.registerBeanDefinition(group + "Api", builder.getRawBeanDefinition());
        });
    }

    private Map<String, List<String>> buildGroup() {
        try {
            Class<?> clazz = Class.forName(tagClass);
            return Arrays.stream(clazz.getClasses())
                    .collect(Collectors.toMap(Class::getSimpleName, this::getApiTags));
        } catch (ClassNotFoundException e) {
            log.error("Class not found: {}", tagClass, e);
            return Collections.emptyMap();
        }
    }

    private List<String> getApiTags(Class<?> cls) {
        return Arrays.stream(cls.getDeclaredFields())
                .filter(field -> Modifier.isFinal(field.getModifiers()))
                .map(field -> {
                    try {
                        return field.get(null).toString();
                    } catch (IllegalAccessException e) {
                        log.error("Failed to access field: {}", field.getName(), e);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private Docket createDocket(String groupName, List<String> apiTags) {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName(groupName)
                .forCodeGeneration(true)
                .select()
                .apis(getControllerPredicate(apiTags)::test)
                .paths(PathSelectors.any())
                .build()
                .apiInfo(buildApiInfo())
                .globalOperationParameters(generateGlobalParameters());

        if (StringUtils.isNotBlank(host)) {
            docket.host(host);
        }
        return docket;
    }

    private java.util.function.Predicate<RequestHandler> getControllerPredicate(List<String> apiTags) {
        java.util.function.Predicate<RequestHandler> controllerPredicate = RequestHandlerSelectors.withClassAnnotation(RestController.class)
                .or(RequestHandlerSelectors.withClassAnnotation(Controller.class));

        return controllerPredicate.and(input -> {
            Optional<Api> apiOptional = input.findControllerAnnotation(Api.class).toJavaUtil();
            return apiOptional.isPresent() && new HashSet<>(apiTags).containsAll(Arrays.asList(apiOptional.get().tags()));
        });
    }

    private ApiInfo buildApiInfo() {
        return new ApiInfoBuilder()
                .title(title)
                .description(description)
                .version(version)
                .termsOfServiceUrl(teamUrl)
                .contact(new Contact(name, teamUrl, eMail))
                .build();
    }

    private List<Parameter> generateGlobalParameters() {
        return Collections.singletonList(
                new ParameterBuilder()
                        .name(RequestHeaderConst.TOKEN)
                        .description("token")
                        .modelRef(new ModelRef("string"))
                        .parameterType("header")
                        .defaultValue("1")
                        .required(false)
                        .build()
        );
    }

    @Override
    public void postProcessBeanFactory(@NotNull ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // No post-processing required
    }
}
